import coin.exchange.base.kr_rest.private_client_base as privcb
import coin.exchange.binance.kr_rest.native_private_client as nprivc
from coin.proto.coin_query_pb2 import (AccountBalance,
                                       ProductOrderElement,
                                       ProductOrderBundle,
                                       CurrencyBalance,
                                       CurrencyTransferBundle,
                                       CurrencyTransferElement,
                                       ProductFillElement,
                                       ProductFillBundle)
from coin.exchange.binance.enum_types import BinanceOrderSide, BinanceOrderType, BinanceTimeInForce
from coin.exchange.binance.kr_rest.currency import BinanceCurrency
from coin.exchange.binance.kr_rest.product import BinanceProduct
import coin.proto.coin_order_enums_pb2 as coin_order
import dateutil.parser

class BinancePrivateParser(object):
  @staticmethod
  def parse_balance(update_msg_list: list, include_zero=True):
    assert isinstance(update_msg_list, list), update_msg_list
    update_msg_list = sorted(update_msg_list, key=lambda elem: elem["asset"])

    account_balance = AccountBalance()
    account_balance.exchange = "Binance"
    for update_msg in update_msg_list:
      currency = BinanceCurrency.FromStrNativeCurrencyNothrow(update_msg['asset'].upper())
      if currency is None:
        continue
      currency_balance = CurrencyBalance()
      currency_balance.currency = currency.currency
      currency_balance.currency_native = currency.native_currency
      currency_balance.available = float(update_msg['free'])
      currency_balance.hold = float(update_msg['locked'])
      currency_balance.total = currency_balance.available + currency_balance.hold
      if include_zero or currency_balance.total > 0:
        account_balance.each_balance.extend([currency_balance])
    return account_balance

  @staticmethod
  def parse_margin_balance(update_msg_list: list, include_zero=True):
    update_msg_list = sorted(update_msg_list, key=lambda elem: elem["asset"])

    account_balance = AccountBalance()
    account_balance.exchange = "Binance"
    for update_msg in update_msg_list:
      currency = BinanceCurrency.FromStrNativeCurrencyNothrow(update_msg['asset'].upper())
      if currency is None:
        continue
      currency_balance = CurrencyBalance()
      currency_balance.currency = currency.currency
      currency_balance.currency_native = currency.native_currency
      currency_balance.available = float(update_msg['free'])
      currency_balance.hold = float(update_msg['locked'])
      currency_balance.total = float(update_msg['netAsset'])
      if include_zero or currency_balance.total != 0:
        account_balance.each_balance.extend([currency_balance])
    return account_balance

  @staticmethod
  def parse_fills(update_msg: list, product) -> ProductFillBundle:
    """Only first 500 filled orders are retrieved."""
    currency_fill_list = []
    for update_elem in update_msg:
      if update_elem.get('isBuyer') is True:
        side = coin_order.BUY_ORDER
      elif update_elem.get('isBuyer') is False:
        side = coin_order.SELL_ORDER
      else:
        side = coin_order.UNKNOWN_ORDER_SIDE
      if update_elem.get('isMaker') is True:
        fill_type = coin_order.MAKER_FILL_TYPE
      elif update_elem.get('isMaker') is False:
        fill_type = coin_order.TAKER_FILL_TYPE
      else:
        fill_type = coin_order.UNKNOWN_FILL_TYPE
      currency_fill = ProductFillElement(
          symbol=product.symbol,
          price=float(update_elem['price']),
          qty=float(update_elem['qty']),
          side=side,
          fill_type=fill_type,
          order_id=str(update_elem['orderId']),
          fill_timestamp=int(update_elem['time'])*10**6,
          fee=float(update_elem['commission']),
          fee_currency=str(update_elem['commissionAsset']))
      currency_fill_list.append(currency_fill)

    return ProductFillBundle(
        exchange='Binance',
        market_type='Spot',
        symbol=product.symbol,
        each_fill=currency_fill_list)

  @staticmethod
  def parse_orders(update_msg: list, product):
    open_order_list = []
    for update_elem in update_msg:
      assert update_elem['symbol'] == product.native_symbol, update_elem
      if update_elem['side'] == 'SELL':
        side = coin_order.SELL_ORDER
      elif update_elem['side'] == 'BUY':
        side = coin_order.BUY_ORDER
      else:
        raise ValueError(update_elem['side'])

      order = ProductOrderElement(symbol=product.symbol,
                                  side=side,
                                  price=float(update_elem['price']),
                                  qty=float(update_elem['origQty']),
                                  order_id=str(update_elem['orderId']))
      open_order_list.append(order)
    return ProductOrderBundle(
        exchange='Binance',
        symbol=product.symbol,
        each_order=open_order_list,
    )

  @staticmethod
  def parse_transfers(withdraw_update_msg, deposite_update_msg, currency):
    """
    Status of withdrawal: 0:Email Sent, 1:Cancelled,
    2:Awaiting Approval, 3:Rejected, 4:Processing, 5:Failure, 6:Completed
    """
    withdraw_status_map = {
      0: 'TRANSFER_ACCEPTED',
      1: 'CANCEL_CONFIRMED',
      2: 'TRANSFER_ACCEPTED',
      3: 'TRANSFER_REJECTED',
      4: 'TRANSFER_ACCEPTED',
      5: 'TRANSFER_REJECTED',
      6: 'TRANSFER_CONFIRMED'
    }
    """
    Status of deposit
    0:pending, 6:credited but cannot withdraw, 1:success
    """
    deposite_status_map = {
      0: 'TRANSFER_ACCEPTED',
      1: 'TRANSFER_CONFIRMED',
      6: 'TRANSFER_ACCEPTED'
    }
    transfer_list = []
    assert isinstance(withdraw_update_msg, list), withdraw_update_msg
    assert isinstance(deposite_update_msg, list), deposite_update_msg
    for transfer_elem in withdraw_update_msg:
      transfer = CurrencyTransferElement(
          currency=currency.currency,
          type='WITHDRAW',
          to_address=transfer_elem['address'],
          qty=float(transfer_elem['amount']),
          fee=float(transfer_elem['transactionFee']),
          external_transfer_id=transfer_elem['txId'],
          status=withdraw_status_map[transfer_elem['status']],
          create_timestamp=int(dateutil.parser.parse(transfer_elem['applyTime']).timestamp() * 1e9)
        )
      transfer_list.append(transfer)
    for transfer_elem in deposite_update_msg:
      transfer = CurrencyTransferElement(
          currency=currency.currency,
          type='DEPOSIT',
          from_address=transfer_elem['address'],
          qty=float(transfer_elem['amount']),
          external_transfer_id=transfer_elem['txId'],
          status=deposite_status_map[transfer_elem['status']],
          create_timestamp=int(transfer_elem['insertTime'] * 1e6)
        )
      transfer_list.append(transfer)
    return CurrencyTransferBundle(exchange='Binance',
                                  market_type='Spot',
                                  currency=currency.currency,
                                  native_currency=currency.native_currency,
                                  each_transfer=transfer_list)

class BinancePrivateClient(privcb.PrivateClientBase):
  ProductType = BinanceProduct

  def __init__(self, key_file):
    privcb.PrivateClientBase.__init__(self)
    self.nprivc = nprivc.BinanceNativePrivateClient(key_file=key_file)

  def query_account_balance_impl(self):
    update = self.nprivc.query_account()
    assert 'balances' in update.msg, update.msg
    update.msg = BinancePrivateParser.parse_balance(update.msg["balances"])
    return update

  def query_fills_impl(self, product):
    update = self.nprivc.query_trade_list(symbol=product.native_symbol)
    update.msg = BinancePrivateParser.parse_fills(update.msg, product)
    return update

  def query_history_fills_impl(self, product, start_time, end_time):
    start_time = int(start_time.timestamp() * 1000)
    end_time = int(end_time.timestamp() * 1000)
    total_fills_list = []
    update = self.nprivc.query_trade_list(symbol=product.native_symbol,
                                          start_time=start_time,
                                          end_time=end_time,
                                          limit=1000)
    total_fills_list.extend(update.msg)
    while len(update.msg) == 1000:
      new_start_time = int(update.msg[-1]['time'] + 1)
      if new_start_time >= end_time:
        break
      update = self.nprivc.query_trade_list(symbol=product.native_symbol,
                                            start_time=new_start_time,
                                            end_time=end_time,
                                            limit=1000)
      total_fills_list.extend(update.msg)
    update.msg = BinancePrivateParser.parse_fills(total_fills_list, product)
    return update

  def query_list_orders_impl(self, product):
    symbol = product.native_symbol
    update = self.nprivc.query_open_orders(symbol=symbol)
    update.msg = BinancePrivateParser.parse_orders(update.msg, product)
    return update

  def query_account_transfer_impl(self, currency):
    currency = BinanceCurrency.FromStr(currency)
    withdraw_update = self.nprivc.query_withdraw_history(currency.native_currency)
    deposit_update = self.nprivc.query_deposit_history(currency.native_currency)
    update = deposit_update
    update.msg = BinancePrivateParser.parse_transfers(withdraw_update.msg,
                                                   deposit_update.msg,
                                                   currency)
    return update

  def query_submit_on_type(self, time_in_force, product, side, price, qty):
    assert price > 0, (product, side, price, qty)
    assert side in ('buy', 'sell'), side
    symbol = product.native_symbol
    price_str = "%.8f" % price
    assert qty >= 1
    return self.nprivc.place_order(time_in_force=BinanceTimeInForce(time_in_force),
                                   symbol=symbol,
                                   order_side=BinanceOrderSide(side.upper()),
                                   order_type=BinanceOrderType.LIMIT,
                                   price=price_str,
                                   quantity=int(qty))

  def query_submit_ioc(self, product, side, price, qty):
    self._check_product_type(product)
    update = self.query_submit_ioc_impl(product, side, price, qty)
    return update

  def query_submit_ioc_impl(self, product, side, price, qty):
    return self.query_submit_on_type('IOC', product, side, price, qty)

  def query_submit_impl(self, product, side, price, qty):
    return self.query_submit_on_type('GTC', product, side, price, qty)

  def query_cancel(self, product, order_id):
    return self.nprivc.cancel_order(symbol=product.native_symbol, order_id=order_id)

  def query_cancel_product_impl(self, product):
    updates = []
    update_list = self.query_list_orders(product)
    for each_order in update_list.msg.each_order:
      updates.append(self.query_cancel(product, each_order.order_id))
    return updates

class BinanceMarginPrivateClient(BinancePrivateClient):
  def query_account_balance_impl(self):
    update = self.nprivc.query_margin_account()
    if update.msg.get("code"):
      raise ValueError(update.msg.get("msg"))
    update.msg = BinancePrivateParser.parse_margin_balance(update.msg["userAssets"], include_zero=False)
    return update
